local M = {}

-- Get a list of dir entries inside `dir` (with the help of libuv).
-- `dir`: directory path
-- `name_type_filter_map`: after applying this function to an entry, if the result
-- is `nil`, then the entry will not be collected, otherwise, the returned string
-- is collected.
--
---@param dir string
---@param name_type_filter_map? fun(name: string, typ: 'file'|'directory'): string?
---@return string[]
function M.listdir_uv(dir, name_type_filter_map)
    if vim.fn.isdirectory(dir) == 0 then return {} end
    local uv = vim.uv
    local data, i, lst = uv.fs_scandir(dir), 1, {}
    assert(data, "scan dir: " .. dir .. " error")

    name_type_filter_map = name_type_filter_map or function(name, _)
        return name
    end

    local en, et = uv.fs_scandir_next(data)
    while en do
        local mapped = name_type_filter_map(en, et)
        if mapped then
            lst[i] = mapped
            i = i + 1
        end
        en, et = uv.fs_scandir_next(data)
    end

    return lst
end

-- Make things look good
---@param theme string?
---@param dark boolean?
---@param cold boolean?
function M.looks(theme, dark, cold)
    vim.opt.background =
        dark and 'dark' or 'light'

    if theme then
        vim.cmd('colorscheme ' .. theme)
    end

    local hl_normal = vim.api.nvim_get_hl(0, { name = "Normal" })

    -- make sure float windows look good
    local hl_normal_float = vim.api.nvim_get_hl(0, { name = 'NormalFloat' })
    local hl_float_border = vim.api.nvim_get_hl(0, { name = 'FloatBorder' })
    vim.api.nvim_set_hl(0, 'NormalFloat', { bg = hl_normal.bg, fg = hl_normal_float.fg })
    vim.api.nvim_set_hl(0, 'FloatBorder', { bg = hl_normal.bg, fg = hl_float_border.fg })

    -- make sure lsp signature helps look good
    vim.api.nvim_set_hl(0, 'LspSignatureActiveParameter', { fg = '#eeeeee', bg = '#0d8575', bold = true })

    local italic_bold_fg = dark and '#eeeeee' or '#000000'
    vim.api.nvim_set_hl(
        0,
        '@markup.italic',
        {
            fg = italic_bold_fg,
            italic = true
        }
    )
    -- vim.api.nvim_set_hl(
    --     0,
    --     '@markup.strong',
    --     {
    --         fg = italic_bold_fg,
    --         bold = true,
    --     }
    -- )

    if not JustTheBase then
        -- make sure Nvimtree windows look good
        vim.api.nvim_set_hl(0, 'NvimTreeWinSeparator', { fg = hl_normal.bg })
    end

    if cold == false then
        return
    end

    -- make sure lsp/diagnostic windows look good
    local border_style = 'single'
    vim.lsp.handlers['textDocument/hover'] =
        vim.lsp.with(vim.lsp.handlers.hover, {
            border = border_style
        })
    vim.lsp.handlers['textDocument/signatureHelp'] =
        vim.lsp.with(vim.lsp.handlers.signature_help, {
            border = border_style
        })
    vim.diagnostic.config {
        float = {
            border = border_style
        }
    }

    -- make sure diagnostic signs look good
    local signs = {
        Error = ' ',
        Warn = ' ',
        Hint = ' ',
        Info = ' '
    }
    for typ, disp in pairs(signs) do
        local name = 'DiagnosticSign' .. typ
        vim.fn.sign_define(name, { text = disp, texthl = name, numhl = name })
    end
end

-- Replace some number of string sequences of certain length in the beginning of
-- lines with a string repeated the same number of times.
---@param seq string the string sequence
---@param seq_replace string the string that would replace each sequence found
function M.leading_seq_replace(seq, seq_replace)
    vim.cmd(string.format(
        [[%%s:\v^(%s)+:\=repeat('%s', len(submatch(0)) / %d):]],
        seq,
        seq_replace,
        string.len(seq)
    ))
    vim.cmd('nohlsearch')
end

---@return number[]
function M.cur_tab_focusable_wins()
    return vim.tbl_filter(
        function(win) return vim.api.nvim_win_get_config(win).focusable end,
        vim.api.nvim_tabpage_list_wins(0)
    )
end

---@param plugin_dir string
---@param init? string[]
---@return string[]
function M.find_plugin_colors(plugin_dir, init)
    if plugin_dir:sub(-1) ~= '/' then plugin_dir = plugin_dir .. '/' end
    local plugs = M.listdir_uv(plugin_dir, function(name, typ)
        return typ == 'directory' and plugin_dir .. name .. '/' or nil
    end)
    local lst = init ~= nil and vim.deepcopy(init) or {}
    for _, d in ipairs(plugs) do
        local colors = M.listdir_uv(d .. 'colors', function(name, typ)
            return typ == 'file' and
                (name:match('(.*)%.vim') or name:match('(.*)%.lua')) or nil
        end)
        for _, color in ipairs(colors) do
            table.insert(lst, color)
        end
    end
    table.sort(lst)
    lst = vim.fn.uniq(lst) --[[@as string[] ]]

    return lst
end

---@param f fun(buf: number)
function M.loaded_bufs_do(f)
    for _, b in ipairs(vim.api.nvim_list_bufs()) do
        if vim.api.nvim_buf_is_loaded(b) then
            f(b)
        end
    end
end

-- Get normalized, '/'-terminated path string of the directory
-- containing the calling source file
---@return string
function M.current_script_dir()
    local src = debug.getinfo(2, 'S').source
    ---@type string?
    -- this line of code below relies on '*' being greedy
    -- starting with @ indicates the source being a file
    local cur_script_dir = src:match('@(.*[/\\])')
    assert(cur_script_dir, "unrecognizable path string: " .. src)
    return vim.fs.normalize(cur_script_dir) .. '/'
end

-- Move currently visually selected lines up or down.
---@param direction 'up'|'down'
function M.visual_move_lines(direction)
    local vstart = vim.fn.line('v')
    local vend = vim.fn.line('.')
    local upwards = direction == 'up'
    if vstart == vend then
        if upwards then
            vim.cmd('move -2')
            vim.fn.feedkeys('ok', 'n')
        else
            vim.cmd('move +1')
            vim.fn.feedkeys('oj', 'n')
        end
        return
    end

    local reverse = false
    if vstart > vend then
        reverse = true
        vstart, vend = vend, vstart
    end
    if (upwards and vstart == 1) or (not upwards and vend == vim.fn.line('$')) then
        return
    end

    local count = vend - vstart + 1
    local below
    local adjust
    if upwards then
        below = vstart - 2
        adjust = reverse and 'o' .. count .. 'k' or 'oko'
    else
        below = vend + 1
        adjust = reverse and 'o' .. count - 1 .. 'k' or 'ojo'
    end

    vim.cmd(string.format('silent %d,%d move %d', vstart, vend, below))
    --       start  end    result             adjust
    -- move  small  big    start ~ [end-1]    `oko`
    --  up   big    small  [start-1] ~ start  `o{line_count}k`
    --
    -- move  small  big    start ~ [end+1]    `ojo`
    -- down  big    small  start ~ [start+1]  `o{line_count - 1}k`
    vim.fn.feedkeys(adjust, 'n')
end

---@return string[]
function M.all_themes()
    local all = vim.fn.getcompletion('', 'color')
    if not JustTheBase then
        all = M.find_plugin_colors(LazyRoot, all)
    end
    return all
end

---@param ft string
function M.jump_to_floatwin_with_ft(ft)
    local api = vim.api
    for _, win in ipairs(api.nvim_tabpage_list_wins(0)) do
        local buf = vim.fn.winbufnr(win)
        if vim.fn.win_gettype(win) == 'popup' and vim.bo[buf].filetype == ft then
            api.nvim_set_current_win(win)
        end
    end
end

---@param prefix string
---@param options string[]
---@return string[]
function M.select_from_prefix(prefix, options)
    if prefix:len() == 0 then
        return options
    end

    local selected = {}
    for _, option in pairs(options) do
        if vim.startswith(option, prefix) then
            table.insert(selected, option)
        end
    end

    return selected
end

return M
